/*
"The contents of this file are subject to the Mozilla Public License
Version 1.1 (the "License"); you may not use this file except in
compliance with the License. You may obtain a copy of the License at
http://www.mozilla.org/MPL/

Software distributed under the License is distributed on an "AS IS"
basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
License for the specific language governing rights and limitations
under the License.

The Original Code is ReportSheet.

The Initial Developer of the Original Code is Amarnath Chatterjee.
Portions created by Amarnath Chatterjee are Copyright (C). All Rights Reserved.

Contributor(s): Amarnath Chatterjee.
*/

package Reporting
{
	import mx.controls.Alert;
	import mx.collections.XMLListCollection;
	import mx.controls.DataGrid;
	import mx.controls.TextInput;
	import mx.controls.dataGridClasses.DataGridColumn;
	import mx.controls.listClasses.IDropInListItemRenderer;
	import mx.core.ClassFactory;
	import mx.events.DataGridEvent;
	
	public class ReportSheet extends DataGrid
	{
		public static const EQU_IDENTIFIER : String = "=";
		public static const COL_IDENTIFIER : String = "c";
		public static const MATH_EXPRN_ATTR : String = "mathExprn";
		public static const LOCK_EXPRN_ATTR : String = "lock";
		private var m_rows : int = 0;
		private var m_cols : int = 0;
		
		private var m_CellDepMgr : MathCellDependency;
		private var m_EquSolver : EquationSolver = new EquationSolver();
		
		
		//Blank xml for initialization.
		//Each xml node will have the the value as data and whatever additional information we need to keep, we got to
		//add it as attributes. Need to fix it in future.
		//Current possible attributes in nodes.
		//backgroundColor
		//fontColor
		//fontStyle
		//fontWeight
		//mathExprn
		//lock
		private var m_XMLData : XML = <xmlroot>
										<root>
											<row>
												<c0></c0>								
											</row>
										</root>									
									</xmlroot>;
																		
									
		//XML Collection to hold the xmldata.
		private var m_XMLCollection : XMLListCollection = new XMLListCollection(m_XMLData.root.row);


//------------------------------------------------------------------------------------------------------------------
//											Initializing Functions.
//------------------------------------------------------------------------------------------------------------------
		
		//Constructor is kept with zero parameters so that it can be used in mxml files.						
		public function ReportSheet()
		{
			//Call base class ctor.
			super();
			
			//Initialize some dummy xml.
			var tempXml : XML = m_XMLData.copy();
			XmlData = tempXml;
			editable = true;
			addEventListener(DataGridEvent.ITEM_EDIT_END, ItemEditEndHandler);
			addEventListener(DataGridEvent.ITEM_EDIT_BEGIN, ItemEditBeginHandler);
		}
		
		//This function creates a dummy xml with 'r' rows and 'c' cols and puts no data in it.
		public function CreateBlankSheet(r : int, c : int) : void
		{
			var newXML : XML = <xmlroot><root></root></xmlroot>;
			
			for(var i : int = 0; i < r ; i++)
			{
				var rxml : XML = <row></row>;
				for(var j : int = 0; j < c ; j++)
				{
					var cxml : XML = new XML("<c"+j+"></c"+j+">");
					rxml.appendChild(cxml.copy());
				}
				newXML.root.appendChild(rxml.copy());				
			}
			
			XmlData = newXML;
		}
		public function get XmlData() : XML
		{
			return m_XMLData.copy();
		}
		
		//Interpret the new xml and initialize accordingly.
		//The input xml must contain <xmlroot><root></root></xmlroot>
		//For the first version we will assume that xml is well formed.
		//For the first version we also assume that there is no dependency between cells exist.
		public function set XmlData(newXml : XML) : void
		{
			//find out row and col info.
			//Wrap it in xmlcollection.
			//assign as data source.
			//create new columns accordingly.
			
			m_rows = parseInt(newXml.root.row.length());
			m_cols = parseInt(newXml.root.row[0].children().length());
			
			//Creating Math Cell dependency Manager
			m_CellDepMgr = new MathCellDependency(m_rows, m_cols);
			
			//Copying the xml.
			m_XMLData = newXml.copy();
			
			//Creating wrapper xml.
			m_XMLCollection = new XMLListCollection(m_XMLData.root.row);
			
			//Assigning xml data as data provider to datagrid. Data grid will try to examine and figure
			//out the spreadsheet data from the source, but it wont be able to.
			dataProvider = m_XMLCollection;
			
			//We need to specify how to read the data from data source a.k.a Provide mapping to columns.
			
			//columns is a base class property. We cannot assign anything directly to it as returns a 
			//new array whenever "get" is called. So we first assemble everything into a new array and
			//assign it to columns property ( calling "set").
			var newCols : Array = new Array();
			
			for ( var i : int = 0; i < m_cols ; i++)
			{
				var tempCol : DataGridColumn = new DataGridColumn();
				
				//dataField property is used to access the source. While rendering it reads each row in the
				//data source one by one. During each iteration the data for a particular column in read by
				//matching the tag against the dataField property.
				tempCol.dataField = COL_IDENTIFIER+i;
				
				//Name to display as column header.
				tempCol.headerText = "Col"+i;
				
				//Setting itemrenderer
				tempCol.itemRenderer = new ClassFactory(CellRenderer);
				
				//add new columns.
				newCols.push(tempCol);
			}
			
			//assiging new array.
			columns = newCols;
			
			//For catching the bubbling events.
			addEventListener(CellInfo.CELLDATA_UPDT, CellValueUpdated);
			
		}
		
		
//------------------------------------------------------------------------------------------------------------------
//											EventHandling Functions.
//------------------------------------------------------------------------------------------------------------------

		//This event handler is for a bubbling event triggered by CellRender in case it finds
		//a change in data. Both Style and data change are costly. We change only if some update
		//is there.
		private function CellValueUpdated(cellEvnt : CellInfo) : void
		{
			var r : int = Number(cellEvnt.row);
			var c : int = Number(cellEvnt.col);
			if(cellEvnt.IsCellDataUpdate)
			{
				try
				{
					SetData(r, c, cellEvnt.data);
				}
				catch(e : Error)
				{
					Alert.show(e.message);
				}
			}
			
			if(cellEvnt.IsStyleUpdated)
			{
				SetBackgroundColor(r, c, cellEvnt.backgroundColor);
				SetFontColor(r,c, cellEvnt.fontColor);
				if(cellEvnt.fontStyle == CellRenderer.FONTNORMAL)
					SetFontStyleNormal(r,c);
				else
					SetFontStyleItalics(r,c);
					
				if(cellEvnt.fontBold == CellRenderer.FONTNORMAL)
					SetFontWeightNormal(r,c);
				else
					SetFontWeightBold(r,c);
			}
		}
		
		//This function handles the default item editing process of DataGrid. At the begining of edit,
		//We have to create the itemEditor , then pass on listdata and data. Then we set the editor with the
		//value of cell, which can be a mathematical equation or some data.
		private function ItemEditBeginHandler(event : DataGridEvent) : void
		{
			//Not preventing the default ItemEditHandler. It seems it decorates the CellEditor
			//Let it do those stuffs. If the cell is locked then blocking all further procedures.
			
			var r : int = event.rowIndex;
			var c : int = event.columnIndex;
			if(CheckLock(r,c))
			{
				event.preventDefault();
				return;
			}
			
			// Creates an item editor.
			createItemEditor(event.columnIndex,event.rowIndex);
			// All item editors must implement the IDropInListItemRenderer interface
			// and the listData property.
			
			// Initialize the listData property of the editor.
			IDropInListItemRenderer(itemEditorInstance).listData = IDropInListItemRenderer(editedItemRenderer).listData;
			
			// Initialize data property.
			itemEditorInstance.data = editedItemRenderer.data;
			
			//Have to checkout first whether there is a mathematical equation defined in it or not.
			var myEditor:TextInput = TextInput(itemEditorInstance);
			var ci : CellInfo = GetCellInfo(event.rowIndex, event.columnIndex);
			
			if(ci.mathExprn != "")
				myEditor.text = "="+ci.mathExprn;
			else
				myEditor.text = ci.data;
			
		}
		
		//This function handles the default item editing process of DataGrid.
		//When item editing ends, We only update cell value if the oldvalue is not equal to newvalue.
		//Secondly, we have to first set data and then get data to set in text input. As the input to the
		//cell might be an mathematical equation.
		private function ItemEditEndHandler(event : DataGridEvent) : void
		{
			// Get the cell editor and cast it to TextInput.
			var myEditor:TextInput = TextInput(event.currentTarget.itemEditorInstance);
			// Get the new value from the editor.
			var newVal:String = myEditor.text;
			// Get the old value. You can acquire the old value like this.
			//var oldVal:String = event.currentTarget.editedItemRenderer.data[event.dataField];
			
			var ci : CellInfo = GetCellInfo(event.rowIndex, event.columnIndex);
			var oldVal : String = "";
			
			if(ci.mathExprn != "")
				oldVal = EQU_IDENTIFIER + ci.mathExprn;
			else
				oldVal = ci.data;
			
			
			//If the old value is not equal to new value set the data.
			if(oldVal != newVal)
			{
				try
				{
					SetData(event.rowIndex, event.columnIndex, newVal);
				}
				catch(e : Error)
				{
					Alert.show(e.message);
				}			
			}
				
			myEditor.text = GetCellInfo(event.rowIndex, event.columnIndex).data;
			
		}
		public function get Rows() : int
		{
			return m_rows;
		}
		public function get Cols() : int
		{
			return m_cols;
		}
		
//------------------------------------------------------------------------------------------------------------------
//											DataSetting Functions.
//------------------------------------------------------------------------------------------------------------------
		
		//This function sets the data for a particular cell. It can be either any normal string.
		//any numeric data or a special equation. Equations start with "EQU_IDENTIFIER"
		//Data cannot be set for a locked cell. In such an attempt it throws error.
		public function SetData(r : int, c : int, data : String) : void
		{
			if(CheckLock(r,c) || CheckParentLock(r,c))
				throw Error("Can't set data for a locked cell(s)");
				
			//If it is an expression then call SetMathEquation.
			//everymath equation starts with =. Keeping excel standards.
			if(data.length > 0 && data.charAt(0) == EQU_IDENTIFIER)
			{
				//If length is not specified in substring it picks as max length allowed.
				var eqn : String = data.substr(1);
				SetMathEquation(r,c, eqn);
			}
			else
			{
				if(r < m_rows && c < m_cols)
				{
					var colId : String = COL_IDENTIFIER+c; 
										
					//If there is a mathematical equation defined in this cell. Remove it first.
					if(m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR] != undefined)
					{
						delete m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR];
						var pci : CellInfo = GetCellInfo(r,c);
						m_CellDepMgr.RemoveAllDependents(pci);
					}
					
					m_XMLData.root.row[r].child(colId)[0] = data;
					CalculateDependentCellValues(r,c);
				}
				else
					throw Error("Report sheet Set data : Invalid r,c index");
			}
		}
		
		//This function is used to set the final data after calculating all values.
		//Earlier this function was clubbed with set data. But it might lead to serious confusions.
		//and can even land into recursion, on minor mistake.
		private function SetCalculatedData(r : int , c : int , data : String) : void
		{
			//Safety check
			if(CheckLock(r,c) || CheckParentLock(r,c))
				throw Error("Can't set data for a locked cell(s)");
				
			var colId : String = COL_IDENTIFIER+c; 
			m_XMLData.root.row[r].child(colId)[0] = data;
			CalculateDependentCellValues(r,c);
			
		}
		//This function retrieves data for a particular cell. Not much used because to read
		//data about any cell GetCellInfo is best.
		public function GetData(r : int, c : int) : String
		{
			if(r < m_rows && c < m_cols)
			{
				var colId : String = COL_IDENTIFIER+c;
				var ci : CellInfo = GetCellInfo(r,c);
				
				//IF there is a mathematical equation then.
				if(ci.mathExprn != "")			
				{
					var eqn : String = EQU_IDENTIFIER + GetMathEquation(r,c);
					return eqn;
				}
				else
				{
					return ci.data;
				}
			}
			else
				throw Error("Report sheet Get data : Invalid r,c index");
				
			return "";				
		}
		
		//This function sets mathematical equation for a particular cell. These are the steps followed.
		//1.	Set mathExprn in xmldata.
		//2.	Set dependencies of current cell with all other cell by parsing the equation.
		//3.	Get required cells to compute the data. Remember that you only first level required cells
		//		and not the deepest level. It is because, while computing the current cell is going to be updated
		//		only. The dependent cells need not have to be calculated.
		//4.	For each required cells get the data and push it in an array needed further to solve the equation.
		//5.	Solve equation using EquationSolver.
		//6.	Call SetData to set the final data , now some other cells might be dependent on this cells, they will
		//		get updated for this event.
		private function SetMathEquation(r : int, c : int, data : String) : void
		{
			if(r < m_rows && c < m_cols)
			{
				//for handling the case of "=". Don't accept invalid equation.
				if(data == "")
					return;
								
				var colId : String = COL_IDENTIFIER+c;
				var prevMathEqn : String = m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR];
				m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR] = data;
				
				var ci : CellInfo = new CellInfo();
				ci.InitCellInfo(r,c, m_XMLData);
			
				try
				{
					m_CellDepMgr.ParseAndSetDependency(data, ci);
				}
				catch(er : Error)
				{
					//do not setup blank equations.
					if(prevMathEqn == "")
					{
						//Just removing equation in this case.
						delete m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR];
					}
					else
						m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR] = prevMathEqn;
						
					throw er;
				}
				
				var NeededCells : Array = m_EquSolver.GetRequiredCells(data);
				var CellValues : Array = new Array();
				
				for( var i : int = 0; i < NeededCells.length; i++)
				{
					ci = GetCellInfo(NeededCells[i].row, NeededCells[i].col);
					CellValues.push(ci);	
				}
				
				var newCellVal : String = m_EquSolver.Solve(data, CellValues);
				SetCalculatedData(r,c, newCellVal);
			}
			else
				throw Error("Report sheet set math equation : Invalid r,c index");
						
		}
		
		//This function returns the math equation set for the cell. In case math equation is not set
		//then it returns the value set in the cell.
		private function GetMathEquation(r : int, c : int) : String
		{
			if(r < m_rows && c < m_cols)
			{			
				var colId : String = COL_IDENTIFIER+c;
				return m_XMLData.root.row[r].child(colId)[0].@[MATH_EXPRN_ATTR];
			}
			else
				throw Error("Report sheet Get math equation : Invalid r,c index");
				
			return "";						
		}
		
		//This function calculates new cell values for all dependent cells of (r,c).
		//It follows the principle of dynamic programming and stores the value of previously
		//calculated cells for further reference. These are the steps followed.
		//1.	Using dependency Manager find out what all cells are dependent on this cell.
		//		In case there is no dependency , return from the function.
		//2.	In case any of the cells are locked don't proceed with cell update at all.
		//3.	For each dependent cells, and according to dependent order. Calculate each cell value and 
		//		keep storing them in an array.
		//		a.	To solve and equation we need the immediate cell values on which the current cell depends
		//		b.	The dependent order will give you current cell also, ignore that.
		//		c.	Store only those values in the array which are not already present.
		private function CalculateDependentCellValues( r : int, c : int ) : void
		{
			//Find out dependencies.
			var changedCell : CellInfo = new CellInfo();
			changedCell.row = r;
			changedCell.col = c;
			
			var dependentOrder : Array = m_CellDepMgr.GetCellsToUpdate(changedCell);
			
			//nothing to update.
			if(dependentOrder == null || dependentOrder.length == 1)
				return;
			
			var CalculatedValues : Array = new Array();
			
			for each( var ci : CellInfo in dependentOrder )
			{
				var fullCellInfo : CellInfo = GetCellInfo(ci.row, ci.col);
				
				if(!(ci.row == r && ci.col == c))
				{
					//Get all other cell data needed to solve.
					var NeededCells : Array = m_EquSolver.GetRequiredCells(fullCellInfo.mathExprn);
					for ( var nci : int = 0; nci < NeededCells.length; nci++)
					{
						//If the required cell data is not been already calculated
						var CellFound : Boolean = false;
						for ( var cva : int = 0; cva < CalculatedValues.length; cva++)
						{
							if(CalculatedValues[cva].CompareIndices(NeededCells[nci]))
							{
								CellFound = true;
								break;
							}								
						}
						
						//If not found then add to cell values.
						if(!CellFound)
						{
							var ndci : CellInfo = GetCellInfo(NeededCells[nci].row, NeededCells[nci].col);
							CalculatedValues.push(ndci);
						}
					}
					//Get the mathematical equation to solve.
					var newValue : String = m_EquSolver.Solve(fullCellInfo.mathExprn, CalculatedValues);
					
					//Set the new value. This function will not throw error because of locks.
					//If any other error occurs then Nothing can't be done.
					SetCalculatedData(ci.row, ci.col, newValue);
					
					//Reload the cell info.
					fullCellInfo = GetCellInfo(ci.row, ci.col);
				}
				CalculatedValues.push( fullCellInfo);
			}
		}
		
		public function GetCellInfo(r : int, c : int) : CellInfo
		{
			var ci : CellInfo = new CellInfo();
			ci.InitCellInfo(r,c, m_XMLData);
			return ci;
		}
		public function ShowCellInfo(r : int, c : int) : void
		{
			GetCellInfo(r,c).ShowCellInformation();
		}
		public function LockCell(r: int, c:int) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[LOCK_EXPRN_ATTR] = true;
		}
		public function UnLockCell(r: int, c:int) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[LOCK_EXPRN_ATTR] = false;
		}
		public function CheckLock(r:int, c:int) : Boolean
		{
			var colId : String = COL_IDENTIFIER+c;
			if(m_XMLData.root.row[r].child(colId)[0].@[LOCK_EXPRN_ATTR] == undefined
				|| m_XMLData.root.row[r].child(colId)[0].@[LOCK_EXPRN_ATTR] == false)
			{
				return false;
			}
			else
			{
				return true;	
			}
		}
		public function CheckParentLock(r: int, c: int) : Boolean
		{
			//Find out dependencies.
			var changedCell : CellInfo = new CellInfo();
			changedCell.row = r;
			changedCell.col = c;
			
			var dependentOrder : Array = m_CellDepMgr.GetCellsToUpdate(changedCell);
			
			if(dependentOrder != null)
			{
				//If any of the dependent cell is locked. Reject all updates.
				for( var li : int = 0; li < dependentOrder.length; li++)
				{
					var inspectCI : CellInfo = GetCellInfo(dependentOrder[li].row, dependentOrder[li].col);
					if(inspectCI.lock == true)
						return true;
				}
			}
			return false;
		}
		
//------------------------------------------------------------------------------------------------------------------
//											Styling Functions.
//------------------------------------------------------------------------------------------------------------------
		public function SetBackgroundColor(r : int, c : int, color : String) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[CellRenderer.BGCOLOR] = color;
		}
		
		public function SetFontColor(r : int, c : int, color : String) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[CellRenderer.FONTCOLOR] = color;
		}
		
		public function SetFontStyleItalics(r : int, c : int) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[CellRenderer.FONTSTYLE] = CellRenderer.FONTITALIC;
		}
		public function SetFontStyleNormal(r : int, c : int) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[CellRenderer.FONTSTYLE] = CellRenderer.FONTNORMAL;
		}
		
		public function SetFontWeightBold(r : int, c : int) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[CellRenderer.FONTWEIGHT] = CellRenderer.FONTBOLD;
		}
		public function SetFontWeightNormal(r : int, c : int) : void
		{
			var colId : String = COL_IDENTIFIER+c;
			m_XMLData.root.row[r].child(colId)[0].@[CellRenderer.FONTWEIGHT] = CellRenderer.FONTNORMAL;
		}
	}
}